package com.ljj.admin.shiro.config;

import cn.hutool.core.codec.Base64;
import com.ljj.admin.shiro.realm.sysUserRealm;
import com.ljj.admin.shiro.services.RetryLimitHashedCredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.*;

/**
 * shiro 整合遇到的问题
 *
 * 1.shiro+redis集成，避免每次访问有权限的链接都会去执行MyShiroRealm.doGetAuthenticationInfo()方法来查询当前用户的权限，
 * 因为实际情况中权限是不会经常变得，这样就可以使用redis进行权限的缓存。
 *
 * 2.实现shiro链接权限的动态加载，之前要添加一个链接的权限，要在shiro的配置文件中添加filterChainDefinitionMap.put(“/add”, “roles[100002]，perms[权限添加]”)，
 * 这样很不方便管理，一种方法是将链接的权限使用数据库进行加载，另一种是通过init配置文件的方式读取。
 *
 * 3.Shiro 自定义权限校验Filter定义，及功能实现。
 *
 * 4.Shiro Ajax请求权限不满足，拦截后解决方案。这里有一个前提，我们知道Ajax不能做页面redirect和forward跳转，
 * 所以Ajax请求假如没登录，那么这个请求给用户的感觉就是没有任何反应，而用户又不知道用户已经退出了。
 *
 * 5.控制同一个用户的在线数量。（挤出之前的登录用户）
 *
 * 6.Shiro 登录后跳转到最后一个访问的页面
 *
 * 7.在线显示，在线用户管理（踢出登录）。
 *
 * 8.登录注册密码加密传输。
 *
 * 9.集成动态验证码。
 *
 * 10.记住我的功能。关闭浏览器后还是登录状态。
 */
@Configuration
public class ShiroConfig {


    @Value("${shiro.user.loginUrl}")
    private String loginUrl;

    @Value("${shiro.user.unauthorizedUrl}")
    private String unauthorizedUrl;

    @Value("${shiro.redis.redisCacheExpire}")
    private int redisCacheExpire;

    ///session在redis中的保存时间,最好大于session会话超时时间
    @Value("${shiro.session.sessionExpire}")
    private int sessionExpire;


    @Value("${shiro.session.validationInterval}")
    private int validationInterval;


    @Value("${shiro.password.algorithmName}")
    private String algorithmName;
    @Value("${shiro.password.hashIterations}")
    private int hashIterations;
    @Value("${shiro.password.reTryCount}")
    private int reTryCount;


    private Logger logger = LoggerFactory.getLogger(this.getClass());


    /**
     *对于经常对同一帐户进行重复身份验证的应用程序（例如，在对每个请求进行身份验证的REST或Soap应用程序中经常执行的操作），启用身份验证缓存以减轻任何后端数据源的恒定负载可能是明智的。
     *如果您的领域实现满足以下任一条件，则仅启用身份验证缓存
     *凭据 已被安全地模糊处理而不是纯文本（原始）凭据。例如，如果您的领域使用密码引用了帐户
     *凭据为纯文本（原始），并且存储实例的缓存区域将不会溢出到磁盘，也不会在不受保护的（非TLS / SSL）网络上传输缓存条目（如联网/分布式企业缓存的情况）。即使在专用/受信任/企业网络中也应如此。
     * @return
     */
    @Bean
    public sysUserRealm myShiroRealm()
    {
        sysUserRealm userRealm =new sysUserRealm();
        RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(redisCacheManager());
        retryLimitHashedCredentialsMatcher.setHashAlgorithmName(algorithmName);
        retryLimitHashedCredentialsMatcher.setHashIterations(hashIterations);
        retryLimitHashedCredentialsMatcher.setReTryCount(reTryCount);

        userRealm.setCachingEnabled(true);

        userRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher);

//        //启用身份验证缓存，即缓存AuthenticationInfo信息，默认false
//        userRealm.setAuthenticationCachingEnabled(true);
//        //缓存AuthenticationInfo信息的缓存名称
//        userRealm.setAuthenticationCacheName("authenticationCache");
        //启用授权缓存，即缓存AuthorizationInfo信息，默认false
        userRealm.setAuthorizationCachingEnabled(true);
        //缓存AuthorizationInfo信息的缓存名称
        userRealm.setAuthorizationCacheName("authorizationCache");


        return userRealm;
    }



    /**
     * 配置核心安全事务管理器
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(sysUserRealm myShiroRealm){

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(myShiroRealm);

        //配置redis缓存
        securityManager.setCacheManager(redisCacheManager());
        //配置自定义session管理，使用redis
        securityManager.setSessionManager(sessionManager());

        // 配置 rememberMeCookie
        securityManager.setRememberMeManager(rememberMeManager());

        return securityManager;
    }

//    /**
//     * 配置Shiro生命周期处理器
//     * 方法 改为 static 类型 ； 非static 类型 会造成 @value 取配置文件值失败
//     * also , be particularly careful with  BeanPostProcessor and BeanFactoryPostProcssor definitions via @Bean
//     * Those should usually be declared as static @Bean methods ,not triggering th instantiation of their containing configuration class,
//     * Otherwise. @Autowired and @Value won't work on the configuration class itself since it is being created as a bean instance too early
//     *
//     * 另外，通过@Bean对BeanPostProcessor和BeanFactoryPostProcssor定义要特别小心，它们通常应该声明为静态的@Bean方法，
//     * 否则不会触发其包含的配置类的实例化。@Autowired和@Value不能在配置类本身上工作，因为它作为bean实例创建得太早了
//     * @return
//     */
//    @Bean(name = "lifecycleBeanPostProcessor")
//    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
//        return new LifecycleBeanPostProcessor();
//    }






    /**
     * 开启shiro 注解模式
     * 可以在controller中的方法前加上注解
     * 如 @RequiresPermissions("userInfo:add")
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }



    /**
     * 解决： 无权限页面不跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized") 无效
     * shiro的源代码ShiroFilterFactoryBean.Java定义的filter必须满足filter instanceof AuthorizationFilter，
     * 只有perms，roles，ssl，rest，port才是属于AuthorizationFilter，而anon，authcBasic，auchc，user是AuthenticationFilter，
     * 所以unauthorizedUrl设置后页面不跳转 Shiro注解模式下，登录失败与没有权限都是通过抛出异常。
     * 并且默认并没有去处理或者捕获这些异常。在SpringMVC下需要配置捕获相应异常来通知用户信息
     * @return
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
        Properties properties=new Properties();
        //这里的 /unauthorized 是页面，不是访问的路径
        properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/403");
        properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/403");
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }






    /**
     * DefaultAdvisorAutoProxyCreator，Spring的一个bean，由Advisor决定对哪些类的方法进行AOP代理。
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * shiro缓存管理器;
     * 需要添加到securityManager中
     * @return
     */
    @Bean
    public RedisCacheManager redisCacheManager(){
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        //redis中针对不同用户缓存
        redisCacheManager.setPrincipalIdFieldName("loginName");
        //用户权限信息缓存时间
        redisCacheManager.setExpire(redisCacheExpire);
        return redisCacheManager;
    }


    @Bean("redisManager")
    public RedisManager redisManager(){
        RedisManager redisManager = new RedisManager();
        return redisManager;
    }

    /**
     * 配置会话ID生成器
     * @return
     */
    @Bean
    public SessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }

    /**
     * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
     * MemorySessionDAO 直接在内存中进行会话维护
     * EnterpriseCacheSessionDAO  提供了缓存功能的会话维护，默认情况下使用MapCache实现，内部使用ConcurrentHashMap保存缓存的会话。
     * @return
     */
    @Bean
    public SessionDAO sessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        //session在redis中的保存时间,最好大于session会话超时时间
        redisSessionDAO.setExpire(sessionExpire);
        return redisSessionDAO;
    }


    /**
     * 配置会话管理器，设定会话超时及保存
     * @return
     */
    @Bean("sessionManager")
    public SessionManager sessionManager() {
        ShiroSessionManager sessionManager = new ShiroSessionManager();
        Collection<SessionListener> listeners = new ArrayList<SessionListener>();


        //配置监听
//        listeners.add(sessionListener());
        sessionManager.setSessionListeners(listeners);
//        sessionManager.setSessionIdCookie(sessionIdCookie());
        sessionManager.setSessionDAO(sessionDAO());
        sessionManager.setCacheManager(redisCacheManager());
//        sessionManager.setSessionFactory(sessionFactory());

        //全局会话超时时间（单位毫秒），默认30分钟  暂时设置为10秒钟 用来测试
        sessionManager.setGlobalSessionTimeout(sessionExpire);
        //是否开启删除无效的session对象  默认为true
        sessionManager.setDeleteInvalidSessions(true);
        //是否开启定时调度器进行检测过期session 默认为true
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
        //设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
        //暂时设置为 5秒 用来测试
        sessionManager.setSessionValidationInterval(validationInterval*1000);
        //取消url 后面的 JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;

    }




    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意：初始化ShiroFilterFactoryBean的时候需要注入：SecurityManager
     * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager defaultWebSecurityManager){
        logger.info("ShiroConfiguration initialized ---ShiroFilterFactoryBean");

        ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();

        // 必须设置 SecurityManager,Shiro的核心安全接口
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);


        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授权界面,该配置无效，并不会进行页面跳转;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        //拦截器.
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();

        //登出
        filterChainDefinitionMap.put("/logout", "logout");

        //<!-- 过滤链定义，从上向下顺序执行，一般将 /**放在最为下边 -->:这是一个坑呢，一不小心代码就不好使了;
        //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        //添加shiro的内置过滤器
        /**
         * anon：无需认证就可以访问
         * authc：必须认证了才能访问
         * user：必须拥有 记住我 功能才能用
         * perms：拥有对某个资源的权限才能访问
         * role：拥有某个角色权限才能访问
         */


        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/lib/**", "anon");


        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/loginIn", "anon");


        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    /**
     * rememberMe cookie 效果是重开浏览器后无需重新登录
     * @return
     */
    private SimpleCookie rememberMeCookie(){
        SimpleCookie cookie=new SimpleCookie("rememberMe");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(300000);
        return cookie;
    }
    /**
     * cookie管理对象
     *
     * @return CookieRememberMeManager
     */
    private CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        // rememberMe cookie 加密的密钥
        // cookieRememberMeManager.setCipherKey用来设置加密的Key,参数类型byte[],字节数组长度要求16
        cookieRememberMeManager.setCipherKey(Base64.decode("ZUdsaGJuSmxibVI2ZHc9PQ=="));
        return cookieRememberMeManager;
    }


//    /**
//     * 必须（thymeleaf页面使用shiro标签控制按钮是否显示）
//     * 未引入thymeleaf包，Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect
//     * @return
//     */
//    @Bean
//    public ShiroDialect shiroDialect() {
//        return new ShiroDialect();
//    }

//    /**
//     * 解决spring-boot Whitelabel Error Page
//     * @return
//     */
//    @Bean
//    public EmbeddedServletContainerCustomizer containerCustomizer() {
//
//        return new EmbeddedServletContainerCustomizer() {
//            @Override
//            public void customize(ConfigurableEmbeddedServletContainer container) {
//
//                ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthorized.html");
//                ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404.html");
//                ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html");
//
//                container.addErrorPages(error401Page, error404Page, error500Page);
//            }
//        };
//    }

}
